结构化输出

如何引导AI结构化输出

  • AI模型输出响应并非像传统程序那样默认按某种结构化响应来输出,而是按照自然语言进行输出。
  • AI模型是可以按照输入的指令、需求来进行特定的处理和输出。那么可以利用这一点,要求AI按照指定的格式结构输出。
  • 所以在调用AI之前,需要在提示词中加入明确、精准的格式指令,且最好是给出格式的例子来进行引导。如果输入的指令不够明确,也会存在一定的风险导致AI无法按照预想输出。
  • 再配合上模型的特定参数进一步要求AI。比如最大输出的数量,防止输出被中途截断;有些模型还具备相应格式的参数。

SpringAI的结构化输出转换器

  • SpringAI提供了结构化输出转换器,原理与上述“引导AI结构化输出”类似,知识进一步做了封装。

    工作流程图

  • 上图是官方的结构化输出转换器工作流程图

    • 主要分为调用AI模型之前和之后
    • 在调用AI之前将转换器的结构化格式追加到输入的提示词之中,为AI输出进行格式引导。
    • 在调用AI之后,在通过转换器将输出的文本内容转换成想要的类型,比如Java类。

如何使用结构化输出转换器

  • 这个在记录“ChatClient(二)”的“自定义响应结构”中就已经用到过的。

  • 实现StructuredOutputConverter接口重写convert(String source)getFormat()方法。

  • 这里再记录一个自定义的JSON结构转Java类的转换器案例

    • 要求AI响应的是指定的Java类的JSON格式的文本字符串,通过JSON Schema来作为示例。
    • 将AI输出的JSON文本内容转换为指定的Java类。
  • 创建一个Json转换用到的工具类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    public class ConvertorUtils {
    private static final ObjectMapper JSON_OBJECT_MAPPER = new ObjectMapper()
    .registerModule(new JavaTimeModule())
    .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
    .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
    .enable(SerializationFeature.INDENT_OUTPUT);

    private static final SchemaGenerator GENERATOR = new SchemaGenerator(new SchemaGeneratorConfigBuilder(
    com.github.victools.jsonschema.generator.SchemaVersion.DRAFT_2020_12,
    com.github.victools.jsonschema.generator.OptionPreset.PLAIN_JSON)
    .with(new JacksonModule(
    JacksonOption.RESPECT_JSONPROPERTY_REQUIRED,
    JacksonOption.RESPECT_JSONPROPERTY_ORDER))
    .with(Option.FORBIDDEN_ADDITIONAL_PROPERTIES_BY_DEFAULT)
    .build());
    /**
    * 生成 Json Schema
    */
    public static String generateSchema(Type clz) {
    JsonNode jsonNode = GENERATOR.generateSchema(clz);
    return toJsonString(jsonNode);
    }
    /**
    * 字符串解析成Java类对象
    */
    public static <T> T parseJsonObject(String json, Type type) {
    try {
    return JSON_OBJECT_MAPPER.readValue(json, JSON_OBJECT_MAPPER.constructType(type));
    } catch (JsonProcessingException jpe) {
    throw new RuntimeException(jpe);
    }
    }
    }
  • 创建一个JsonStructuredConverter实现StructuredOutputConverter接口。要求遵循输入的JSON Schema来输出对应的Json格式文本。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    @Slf4j
    public class JsonStructuredConverter<T> implements StructuredOutputConverter<T> {
    /** 输出格式的要求提示词模版 */
    private final String FORMAT_TEMPLATE = """
    你的响应格式必须是JSON格式。
    不用做任何解释,只提供符合RFC8259的JSON响应。
    不要在响应中包含markdown代码块,且从输出中删除``json markdown。
    必须遵守以上要求,不可以有任何偏差。
    下列是你输出必须遵循的JSON Schema实例:
    ```%s```
    """;

    /** Java 的类型 */
    private ParameterizedTypeReference<T> reference;

    public JsonStructuredConverter(Class<T> clz) {
    this.reference = ParameterizedTypeReference.forType(clz);
    }

    public JsonStructuredConverter(ParameterizedTypeReference<T> reference) {
    this.reference = reference;
    }

    @Override
    public String getFormat() {
    String schema = ConvertorUtils.generateSchema(reference.getType());
    log.info("\ngetFormat schema -> {}", schema);
    // 生成带有 Json Schema 的输出格式提示词
    return String.format(FORMAT_TEMPLATE, schema);
    }

    @Override
    public T convert(String text) {
    log.info("\nconvert text -> {}", text);
    // 将AI响应输出的文本转换成Java类
    return ConvertorUtils.parseJsonObject(text.trim(), reference.getType());
    }
    }
  • 定义响应的Java类,并使用@JsonClassDescription写上字段的Json描述

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @JsonClassDescription("这是输出中国王朝需要遵守的JSON格式")
    public record ChineseDynasties(@JsonPropertyDescription("王朝名称") String dynasty,
    @JsonPropertyDescription("王朝存续时长") int reignDuration,
    @JsonPropertyDescription("王朝建立时间") String beginAt,
    @JsonPropertyDescription("王朝灭亡时间") String endAt,
    @JsonPropertyDescription("王朝一共在位皇帝数量") int emperorCount,
    @JsonPropertyDescription("王朝的第一位皇帝") String firstEmperor,
    @JsonPropertyDescription("王朝的最后一位皇帝") String lastEmperor) {

    }
  • 调用及结果(模型使用的Deepseek)

    • 调用案例

      1
      2
      3
      4
      5
      6
      7
      8
      private final ChatClient promptClient;

      public void example() {
      ChineseDynasties entity = promptClient.prompt()
      .user("请列出中国古代统治时间最长的一个大一统王朝。")
      .call()
      .entity(new JsonStructuredConverter<>(ChineseDynasties.class));
      }
    • 输出的Json Schema。关于Json Schema可以点击👉查看

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      {
      "$schema" : "https://json-schema.org/draft/2020-12/schema",
      "type" : "object",
      "properties" : {
      "beginAt" : {
      "type" : "string",
      "description" : "王朝建立时间"
      },
      "dynasty" : {
      "type" : "string",
      "description" : "王朝名称"
      },
      "emperorCount" : {
      "type" : "integer",
      "description" : "王朝一共在位皇帝数量"
      },
      "endAt" : {
      "type" : "string",
      "description" : "王朝灭亡时间"
      },
      "firstEmperor" : {
      "type" : "string",
      "description" : "王朝的第一位皇帝"
      },
      "lastEmperor" : {
      "type" : "string",
      "description" : "王朝的最后一位皇帝"
      },
      "reignDuration" : {
      "type" : "integer",
      "description" : "王朝存续时长"
      }
      },
      "description" : "这是输出中国王朝需要遵守的JSON格式",
      "additionalProperties" : false
      }
    • 请求和响应的输出。响应是按照我们的Json Schema输出的Json文本。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      2025-07-07T14:06:12.760+08:00  INFO 47349 --- [spring-ai-example] [           main] c.s.a.e.advisor.three.LogExampleAdvisor  : 
      Chat client request to AI
      prompt text -> 请列出中国古代统治时间最长的一个大一统王朝。
      context -> {
      "spring.ai.chat.client.output.format" : "你的响应格式必须是JSON格式。\n不用做任何解释,只提供符合RFC8259的JSON响应。\n不要在响应中包含markdown代码块,且从输出中删除``json markdown。\n必须遵守以上要求,不可以有任何偏差。\n下列是你输出必须遵循的JSON Schema实例:\n```{\n \"$schema\" : \"https://json-schema.org/draft/2020-12/schema\",\n \"type\" : \"object\",\n \"properties\" : {\n \"beginAt\" : {\n \"type\" : \"string\",\n \"description\" : \"王朝建立时间\"\n },\n \"dynasty\" : {\n \"type\" : \"string\",\n \"description\" : \"王朝名称\"\n },\n \"emperorCount\" : {\n \"type\" : \"integer\",\n \"description\" : \"王朝一共在位皇帝数量\"\n },\n \"endAt\" : {\n \"type\" : \"string\",\n \"description\" : \"王朝灭亡时间\"\n },\n \"firstEmperor\" : {\n \"type\" : \"string\",\n \"description\" : \"王朝的第一位皇帝\"\n },\n \"lastEmperor\" : {\n \"type\" : \"string\",\n \"description\" : \"王朝的最后一位皇帝\"\n },\n \"reignDuration\" : {\n \"type\" : \"integer\",\n \"description\" : \"王朝存续时长\"\n }\n },\n \"description\" : \"这是输出中国王朝需要遵守的JSON格式\",\n \"additionalProperties\" : false\n}```\n",
      "ClientName" : "promptClient"
      }
      2025-07-07T14:06:19.980+08:00 INFO 47349 --- [spring-ai-example] [ main] c.s.a.e.advisor.three.LogExampleAdvisor :
      Chat client response from AI
      output text -> {
      "beginAt": "公元前202年",
      "dynasty": "汉朝",
      "emperorCount": 29,
      "endAt": "公元220年",
      "firstEmperor": "汉高祖刘邦",
      "lastEmperor": "汉献帝刘协",
      "reignDuration": 422
      }
    • 仔细观察会发现,SpringAI是将输出格式的提示词放在Advisor的上下文中,key是spring.ai.chat.client.output.format

SpringAI封装好的转换器

BeanOutputConverter

  • 与上面自定义的Json转换器类似(其实就是参考这个)。
  • 通过提示词引导 AI 模型生成符合 DRAFT_2020_12JSON Schema (基于指定 Java 类生成)的响应输出。
  • 后用 ObjectMapper 将输出的 JSON 文本反序列化为目标类的 Java 对象实例。

MapOutputConverter

  • 引导 AI 模型生成符合 RFC8259 标准的 JSON 响应。
  • 使用 MessageConverter 将输出的JSON 文本转换为 java.util.Map<String, Object> 对象。
  • 继承了AbstractMessageOutputConverter<T>使用的MessageConverter进行转换。

ListOutputConverter

  • 引导 AI 模型使用英文逗号,为分隔符返回列表响应。
  • 使用 ConversionService 将将输出的列表文本转换为 java.util.List<String>
  • 继承了AbstractConversionServiceOutputConverter<T>使用的ConversionService进行转换。
  • 这些转换器使用都是一样的。

家族图谱

  • 下面是官方的类图

    结构化输出类体系结构

  • 最上层的FormatProviderConverter<S, T>接口分别对应着getFormat()convert(S source)方法。

  • StructuredOutputConverter<T>默认了转换的输入内容是String类型的文本。

  • 关于AbstractMessageOutputConverterAbstractConversionServiceOutputConverter,理论上这两个应该与BeanOutputConverter是同辈,都是分别组合不同了的转换工具来进行转换。

转换器是如何介入与AI交互过程的

  • 在前面的案例看到了SpringAI是将输出格式的提示词放在Advisor的上下文中,那在何时追加到提示词消息中呢?

  • 通过简单翻看了下源码,发现是在ChatModelCallAdvisor中与AI交互之前,通过Prompt对提示词增强修改追加在UserMessage中了。如下图:

    ChatModelCallAdvisor

  • ChatModelCallAdvisor中有响应格式的提示词增强,那ChatModelStreamAdvisor中有没有?答案是没有的。

    ChatModelStreamAdvisor

  • 这里如果说stream模式的请求无法做Convert,但实际中应该还是会需要Format指定输出格式吧。看下来使用stream模式是没有参数可传入的,以后看会不会升级,目前是只能在Advisor中自己实现了。

  • 另外至于转换,是在与AI交互响应后整个Advisor链执行完了再调用的.convert()方法。这里源码比较分散,就不放图了。

总结

  • AI模型结构化输出需要通过提示词引导AI按照指定格式输出。
  • SpringAI提供了格式化输出转换器 - StructuredOutputConverter
    • 可以通过实现StructuredOutputConverter自定义转换器
    • 也可以使用SpringAI封装的三个转换器
      • BeanOutputConverter
      • MapOutputConverter
      • ListOutputConverter
  • SpringAI是在ChatModelCallAdvisor与AI交互之前将结构化输出的提示词追加到UserMessage中。
  • ChatModelStreamAdvisor是无法使用转化器的。
  • 最后在AI交互响应后整个Advisor链执行完成调用.convert()方法进行转换

最后

  • 结构化输出是决定了AI应用编程的程序可控以及是否可持续运行。
  • 不过现在看下来有不少模型都是支持JSON_OBJECT响应模式了。
  • 后面继续学习多模态的内容。感觉最近学习有点脱节,进度有点慢,继续加油吧。
  • 所有案例的源码,都会提交在GitHub上。包:com.spring.ai.example.structured